Deep Learning groep 3¶
Team Diep Leren en Opereren Groepsleden:
- Casper Priem - casperpriem
- Lars Timmer - laltir
- Matthias de Vreede - matthiasdevreede
- Willem van Dijk - willemvandijk
Pre-requisites¶
import pandas as pd
import os
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, losses, metrics, regularizers
from sklearn.model_selection import train_test_split
import geopandas as gpd
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization, Input, LeakyReLU, ELU
from tensorflow.keras.regularizers import l2
from tensorflow.keras.callbacks import EarlyStopping
import keras_tuner as kt
from sklearn.metrics import mean_absolute_percentage_error
from tensorflow.keras.utils import plot_model
import contextily as ctx
from sklearn.cluster import KMeans
import datetime as dt
import pathlib
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from tensorflow.keras.applications import ResNet50
from functions import *
import warnings
warnings.filterwarnings("ignore", category=UserWarning)
Exploratory Data Analysis¶
First Look¶
df = pd.read_csv('train.csv')
df.info()
df.describe()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 500 entries, 0 to 499 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 House ID 500 non-null int64 1 Bedrooms 500 non-null int64 2 Bathrooms 500 non-null float64 3 Area 500 non-null int64 4 Latitude 500 non-null float64 5 Longitude 500 non-null float64 6 Price 500 non-null int64 dtypes: float64(3), int64(4) memory usage: 27.5 KB
| House ID | Bedrooms | Bathrooms | Area | Latitude | Longitude | Price | |
|---|---|---|---|---|---|---|---|
| count | 500.000000 | 500.000000 | 500.000000 | 500.00000 | 500.000000 | 500.000000 | 5.000000e+02 |
| mean | 250.500000 | 3.350000 | 2.669500 | 2369.64600 | 35.128844 | -116.469049 | 5.980218e+05 |
| std | 144.481833 | 1.172283 | 1.017578 | 1208.18261 | 2.386173 | 6.554434 | 5.240649e+05 |
| min | 1.000000 | 1.000000 | 1.000000 | 701.00000 | 32.631500 | -122.368300 | 2.200000e+04 |
| 25% | 125.750000 | 2.000000 | 2.000000 | 1440.00000 | 33.808200 | -119.802500 | 2.350000e+05 |
| 50% | 250.500000 | 3.000000 | 2.500000 | 2097.50000 | 33.920800 | -117.609600 | 5.300000e+05 |
| 75% | 375.250000 | 4.000000 | 3.000000 | 3104.50000 | 35.640600 | -116.371300 | 7.490000e+05 |
| max | 500.000000 | 10.000000 | 7.000000 | 9583.00000 | 47.791800 | -87.885900 | 5.858000e+06 |
Plot graphs¶
fig, ax = plt.subplots(figsize=(10, 6))
plt.scatter(df['Longitude'], df['Latitude'],
c=df['Price'], cmap='viridis')
plt.colorbar(label='Price')
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.title("Geographic Distribution of Houses")
ctx.add_basemap(ax, crs="EPSG:4326")
plt.show()
Analyse van de Geografische Spreiding en Prijsdistributie:
Visualisatie: De scatterplot visualiseert de geografische locaties van de huizen in de dataset aan de hand van hun latitude (Y-as) en longitude (X-as). De kleur van elk punt representeert de verkoopprijs ('Price'), waarbij paarstinten lagere prijzen en geeltinten hogere prijzen aangeven, zoals af te lezen is uit de color bar (variërend van circa 0 tot boven 5.5 miljoen dollar).
Ruimtelijke Clustering en Locaties: De scatterplot visualiseert de locaties via latitude en longitude. De data vertoont duidelijke ruimtelijke clustering.
- Hoofdcluster (Westkust): Een grote concentratie punten ligt tussen circa -123° en -115° longitude. Hoewel dit grotendeels overeenkomt met Californië, bevestigt de aanwezigheid van punten op hogere latitudes (tot ~48°N) dat niet alle data uit Californië komt. Een voorbeeld hiervan is een locatie zoals 48°24'12.2"N 120°10'26.5"W, wat in Washington State ligt (Bron: Google Maps) en overeenkomt met een van de noordelijke punten in de plot binnen dit longitude-bereik.
- Andere Clusters: Er zijn duidelijk aparte, kleinere clusters zichtbaar rond -90° longitude (Midwest/Zuid VS) en -110° longitude (Intermountain West).
- Dit bevestigt de uitspraak uit de opdrachtbeschrijving dat de data meestal uit Californië komt, maar ook andere locaties bevat.
Prijsvariatie en Geografische Correlatie:
- De hoogste prijzen (gele punten, > $4M) lijken geconcentreerd te zijn binnen het hoofdcluster aan de Westkust, met name in het zuidelijkere deel (rond 34°-38° latitude). Binnen dit cluster is er echter ook een grote spreiding in prijzen, met veel woningen in lagere (paars/blauw) en middelhoge (groen/cyaan) prijsklassen.
- De huizen in de clusters rond -90° en -110° longitude lijken systematisch lagere prijzen te hebben. De meeste punten hier vallen in de paarse/donkerblauwe range (waarschijnlijk < $1.5M), met enkele uitschieters naar de blauw/cyaan range. Er zijn geen extreem dure (gele) huizen zichtbaar in deze clusters.
- De noordelijke outliers binnen het Westkust longitude-bereik (bv. rond 47.5° latitude) lijken relatief duur te zijn (groen/geel).
Implicaties voor Modellering:
- De sterke geografische clustering en de duidelijke correlatie tussen locatie en prijs suggereren dat latitude en longitude belangrijke predictoren zullen zijn voor de verkoopprijs.
- De ruimtelijke heterogeniteit (verschillen tussen regio's) is significant. Een model moet in staat zijn deze ruimtelijke patronen te leren. Dit kan betekenen dat de coördinaten direct als input features gebruikt worden, of dat er mogelijk feature engineering nodig is om regio's te identificeren.
- De aanwezigheid van extreem hoge prijzen geconcentreerd in één regio (Californië) kan de prestaties van een model beïnvloeden en vereist mogelijk speciale aandacht (bv. robuuste scaling, transformatie van de targetvariabele, of het gebruik van modellen die goed omgaan met dergelijke distributies).
Samengevat toont de plot aan dat de dataset geografisch geconcentreerd is in specifieke regio's (voornamelijk Californië) en dat er een sterke relatie bestaat tussen de locatie en de huizenprijs, waarbij de duurste huizen zich in het Californische cluster bevinden.
numeric_cols = ["Bedrooms", "Bathrooms", "Area", "Latitude", "Longitude", "Price"]
df[numeric_cols].hist(figsize=(12, 8), bins=20)
plt.tight_layout()
plt.show()
Analyse van Variabelen Distributies:
Bedrooms (Slaapkamers):
- Type: Discrete numerieke variabele.
- Verdeling: De verdeling lijkt enigszins symmetrisch en unimodaal, gecentreerd rond 3-4 slaapkamers. De hoogste frequentie ligt bij 3 slaapkamers, dicht gevolgd door 4. Er is een snelle afname in frequentie voor huizen met meer dan 5 slaapkamers. Huizen met 2 slaapkamers komen ook relatief vaak voor.
- Range: De meeste huizen hebben tussen de 2 en 5 slaapkamers. Waarden boven de 6 zijn zeldzaam.
- Implicaties: Standaard numerieke behandeling lijkt mogelijk, al kan het vanwege het discrete karakter ook als ordinaal worden beschouwd.
Bathrooms (Badkamers):
- Type: Discrete numerieke variabele.
- Verdeling: De verdeling is duidelijk rechtsscheef (positively skewed). Er is een sterke piek bij 2 badkamers. De frequentie neemt sterk af naarmate het aantal badkamers toeneemt. 1 en 3 badkamers komen ook redelijk vaak voor.
- Range: Meestal tussen 1 en 3 badkamers. Waarden boven de 4 zijn zeldzaam.
- Implicaties: De scheefheid kan relevant zijn voor sommige modellen.
Area (Oppervlakte):
- Type: Continue numerieke variabele (waarschijnlijk in vierkante voet).
- Verdeling: Sterk rechtsscheef. De overgrote meerderheid van de huizen heeft een relatief kleine oppervlakte (piek rond 1500-2000 sq ft). Er is een lange staart naar rechts, wat duidt op enkele huizen met een zeer grote oppervlakte (outliers > 6000 sq ft).
- Range: De meeste huizen lijken een oppervlakte < 4000 sq ft te hebben, maar de range strekt zich uit tot bijna 10000 sq ft.
- Implicaties: De sterke scheefheid suggereert dat een transformatie (bv. log-transformatie) van deze feature nuttig kan zijn voor modellen die gevoelig zijn voor de schaal of distributie van features.
Latitude (Breedtegraad):
- Type: Continue numerieke variabele.
- Verdeling: Multimodaal. Er is een dominante piek rond 34 graden (Zuid-Californië?). Kleinere pieken zijn zichtbaar rond 38-39 graden, en mogelijk zwakkere pieken rond 41-42 graden en nabij 48 graden. Er zijn ook gebieden met lagere frequenties tussen deze pieken.
- Range: Ligt tussen circa 32.5 en 48 graden.
- Implicaties: Bevestigt de geografische clustering die in de scatterplot werd waargenomen. De pieken corresponderen met de dichtbevolkte dataregio's op de kaart. De multimodale aard benadrukt het belang van locatie.
Longitude (Lengtegraad):
- Type: Continue numerieke variabele.
- Verdeling: Multimodaal. Er zijn duidelijke pieken die de geografische clusters bevestigen: de hoogste piek ligt rond -118 graden, een andere grote piek rond -122 graden. Kleinere, maar onderscheidende pieken liggen rond -110 graden en -90 graden.
- Range: Ligt tussen circa -123 en -88 graden.
- Implicaties: Bevestigt en kwantificeert de observaties uit de scatterplot over de locaties van de clusters. Longitude is, net als latitude, een belangrijke ruimtelijke feature.
Price (Prijs):
- Type: Continue numerieke variabele (target variabele).
- Verdeling: Extreem rechtsscheef. De overgrote meerderheid van de huizenprijzen is geconcentreerd aan de lage kant (piek < $0.5M, meeste data < $1M-$1.5M). Er is een zeer lange staart naar rechts met enkele extreem dure huizen (outliers tot $6M).
- Range: Strekt zich uit van nabij 0 tot ongeveer $6M.
- Implicaties: Dit is een klassieke verdeling voor prijsdata.
Samenvattend laten de histogrammen belangrijke kenmerken zien zoals scheefheid (Area, Price, Bathrooms) en multimodaliteit (Latitude, Longitude), wat directe input geeft voor feature engineering (bv. transformaties) en bevestigt dat locatie een cruciale rol speelt. \
plt.figure(figsize=(5, 4))
sns.scatterplot(x="Area", y="Price", data=df)
plt.title("relation between Area en Price")
plt.show()
Analyse van de Relatie tussen Oppervlakte en Prijs:
Algemene Trend: De scatterplot toont een duidelijke positieve correlatie tussen 'Area' en 'Price'. Over het algemeen geldt: hoe groter de oppervlakte ('Area' op de X-as), hoe hoger de prijs ('Price' op de Y-as). Dit is intuïtief logisch en bevestigt dat 'Area' een relevante predictor is voor de target variabele 'Price'.
Heteroscedasticiteit: De relatie is niet perfect lineair en vertoont duidelijke heteroscedasticiteit. Dit betekent dat de spreiding (variantie) van de 'Price' toeneemt naarmate de 'Area' groter wordt. Bij kleinere oppervlaktes (bv. < 2000 sq ft) liggen de prijzen relatief dicht bij elkaar (meestal onder $1M). Bij grotere oppervlaktes (> 4000 sq ft) is de spreiding in prijzen veel groter; er zijn zowel relatief betaalbare grote huizen als extreem dure grote huizen. De punten waaieren dus uit naar rechtsboven.
Data Concentratie: De meeste datapunten zijn geconcentreerd in het onderste linkerdeel van de grafiek (kleine oppervlakte, lagere prijs). Dit komt overeen met de rechtsscheve verdelingen die we eerder zagen in de histogrammen voor zowel 'Area' als 'Price'.
Outliers: Er zijn enkele opvallende punten:
- Huizen met een zeer grote oppervlakte (> 7000 sq ft) maar relatief gematigde prijzen (bv. rond $0.5M - $1M).
- Huizen met een gemiddelde tot grote oppervlakte (bv. 2500 - 6000 sq ft) die extreem duur zijn (prijzen > $3M, oplopend tot bijna $6M). Deze punten dragen significant bij aan de heteroscedasticiteit.
Implicaties voor Modellering:
- De positieve correlatie bevestigt de relevantie van 'Area' als input feature.
- Het neurale netwerk zal de non-lineaire relatie en de toenemende variantie (heteroscedasticiteit) moeten leren modelleren. Neurale netwerken zijn hier in principe goed toe in staat.
Samengevat: 'Area' is een belangrijke predictor met een positieve, maar heteroscedastische relatie tot 'Price'. De meeste data bevindt zich bij lagere oppervlaktes en prijzen, maar de variantie neemt toe voor grotere huizen. Dit patroon is een belangrijk kenmerk waarmee het neurale netwerk moet omgaan.
Correlations¶
corr = df[numeric_cols].corr()
plt.figure(figsize=(8, 6))
sns.heatmap(corr, annot=True, cmap='coolwarm', vmin=-1, vmax=1)
plt.title("Correlation matrix")
plt.show()
Analyse van de Correlatiematrix:
Visualisatie: De heatmap toont de Pearson correlatiecoëfficiënten tussen alle numerieke variabelen in de dataset. Rode kleuren duiden op een positieve correlatie (dichter bij +1), blauwe kleuren op een negatieve correlatie (dichter bij -1), en kleuren rond wit/lichtgrijs duiden op een zwakke of geen lineaire correlatie (dicht bij 0).
Correlaties met de Target Variabele ('Price'): We kijken naar de laatste rij (of kolom) om de lineaire relatie van elke feature met de verkoopprijs te zien:
- Area (Oppervlakte): 0.51. Dit is een matig sterke positieve correlatie. Dit bevestigt de bevinding uit de scatterplot dat grotere huizen over het algemeen duurder zijn.
- Bathrooms (Badkamers): 0.51. Eveneens een matig sterke positieve correlatie, vergelijkbaar met 'Area'. Meer badkamers hangt duidelijk samen met een hogere prijs.
- Bedrooms (Slaapkamers): 0.41. Een gematigde positieve correlatie. Het aantal slaapkamers correleert ook positief met de prijs, maar iets minder sterk dan oppervlakte of aantal badkamers.
- Longitude (Lengtegraad): -0.15. Een zwakke negatieve correlatie. Er is een lichte neiging dat huizen verder naar het westen (negatievere longitude) iets duurder zijn, maar de lineaire relatie is zwak.
- Latitude (Breedtegraad): -0.048. Een zeer zwakke negatieve correlatie, praktisch dicht bij nul. Dit suggereert dat er over de gehele dataset geen significante lineaire trend is tussen latitude en prijs.
Correlaties Tussen Input Features (Multicollineariteit): We kijken naar de correlaties tussen de input variabelen onderling:
- Area, Bathrooms, Bedrooms: Er bestaan sterke positieve correlaties tussen deze drie variabelen:
AreavsBathrooms: 0.74AreavsBedrooms: 0.71BathroomsvsBedrooms: 0.74
- Latitude en Longitude: Deze hebben een zwakke positieve correlatie (0.20).
- Locatie vs Grootte/Kamers: De correlaties tussen locatie (Lat/Lon) en de grootte/kamer-features zijn over het algemeen zwak (meestal < |0.2|).
- Area, Bathrooms, Bedrooms: Er bestaan sterke positieve correlaties tussen deze drie variabelen:
Implicaties voor Modellering:
- Belangrijke Predictoren: 'Area', 'Bathrooms' en 'Bedrooms' lijken de sterkste lineaire verbanden te hebben met 'Price' en zijn waarschijnlijk belangrijke predictoren.
- Locatie Features: De zwakke lineaire correlaties van 'Latitude' en 'Longitude' met 'Price' betekenen niet dat locatie onbelangrijk is. De eerdere geografische plots toonden duidelijke, maar niet-lineaire, regionale prijsverschillen. Een neuraal netwerk is juist goed in het oppikken van zulke complexe, niet-lineaire verbanden die een simpele correlatiecoëfficiënt mist. Het is dus nog steeds cruciaal om Lat/Lon mee te nemen.
- Multicollineariteit: De hoge correlatie tussen 'Area', 'Bathrooms' en 'Bedrooms' is een teken van multicollineariteit. Hoewel neurale netwerken hier over het algemeen robuuster tegen zijn dan bijvoorbeeld lineaire regressie (het beïnvloedt de interpreteerbaarheid van gewichten meer dan de voorspellende kracht), betekent het wel dat deze features deels overlappende informatie bevatten. Het is meestal geen reden om features te verwijderen voor een neuraal netwerk, maar het is goed om te weten.
Samenvattend bevestigt de correlatiematrix dat grootte-gerelateerde features (Area, Bathrooms, Bedrooms) lineair sterk samenhangen met de prijs en ook sterk met elkaar. Locatie (Latitude, Longitude) toont weinig lineaire correlatie met prijs op dataset-niveau, maar de eerdere EDA suggereert dat de ruimtelijke relatie complexer is en dus relevant blijft voor een non-lineair model zoals een neuraal netwerk.
Outliers¶
Q1 = df["Price"].quantile(0.25)
Q3 = df["Price"].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5*IQR
upper_bound = Q3 + 1.5*IQR
outliers = df[(df["Price"] < lower_bound) | (df["Price"] > upper_bound)]
print("Amount of outliers :", outliers.shape[0])
Amount of outliers : 23
display_house(1, df, folder='Train/house') # Display the house with ID 1, ID can be changed to any other ID
Analyse van de Voorbeeldafbeelding:
Structuur en Bron: De afbeelding is een compositie van vier individuele foto's, samengevoegd in een 2x2 grid. Elke sub-afbeelding representeert een specifiek deel van het huis: Badkamer (linksboven), Slaapkamer (rechtsboven), Keuken (linksonder), en Vooraanzicht (rechtsonder). Dit bevestigt de structuur van de beelddata zoals beschreven in de opdracht. Boven de afbeelding staat het ID (1) en de bijbehorende prijs ($869,500) vermeld.
Visuele Kenmerken (Huis ID 1):
- Interieur: De interieurfoto's (badkamer, slaapkamer, keuken) tonen ruimtes die redelijk modern en goed onderhouden lijken. De keuken heeft houten kasten en een eethoek, de slaapkamer is ruim met meubilair, en de badkamer heeft standaard voorzieningen.
- Exterieur: Het vooraanzicht toont een huis (waarschijnlijk gelijkvloers) met een architectuurstijl die past bij een warm klimaat (stucwerk, mogelijk mediterraan of zuidwestelijk VS). De landscaping met grind en cactussen versterkt dit beeld.
- Algemene Indruk: Het huis wekt de indruk van een redelijk luxe, goed onderhouden woning in een warme/droge regio. De prijs van $869,500 (in 2016 ) lijkt plausibel voor zo'n type huis in bepaalde Amerikaanse markten.
Informatiewaarde voor Modellen: Deze afbeelding illustreert het type visuele informatie dat de Convolutional Neural Networks (CNNs) zullen ontvangen. Het doel is dat het model kenmerken uit deze beelden leert die correleren met de prijs, zoals:
- Grootte en Indeling: Indirect af te leiden uit de ruimtelijkheid van de kamers en het vooraanzicht.
- Kwaliteit en Onderhoud: Zichtbaar in de afwerking, staat van de keuken/badkamer, en het exterieur.
- Stijl en Moderniteit: Kan indicatief zijn voor waarde.
- Locatiekenmerken: Soms af te leiden uit de architectuur en landscaping (bv. stedelijk vs. landelijk, klimaatzone).
Overwegingen voor Beeldmodellering:
- Variabiliteit: Dit is slechts één voorbeeld. De kwaliteit, belichting, camerahoeken, en de staat/stijl van de huizen zullen aanzienlijk variëren over de gehele dataset. Het model moet robuust zijn voor deze variaties.
- Informatie per Quadrant: Het model moet leren om relevante informatie uit alle vier de sub-afbeeldingen te halen en te integreren.
- Combinatie met Tabular Data: Voor het multimodale model is de uitdaging om de informatie uit deze samengestelde afbeelding effectief te combineren met de numerieke data (aantal kamers, oppervlakte, locatie ) voor hetzelfde huis om tot een nauwkeurigere prijsvoorspelling te komen dan met alleen beeld of alleen tabeldata mogelijk is.
Deze visualisatie is nuttig om een gevoel te krijgen bij de beelddata waarmee de neurale netwerken getraind zullen worden en onderstreept het multimodale karakter van de opdracht.
Conclusie van de EDA
De EDA is uitgevoerd op een dataset bestaande uit zowel tabulaire data (aantal slaapkamers, badkamers, oppervlakte, locatiecoördinaten) als beelddata (samengevoegde foto's van slaapkamer, badkamer, keuken, vooraanzicht) van huizen in de VS uit 2016. Het doel van de EDA was om inzicht te krijgen in de data ter voorbereiding op het ontwikkelen van deep learning modellen voor het voorspellen van de verkoopprijs ('Price').
Belangrijkste Bevindingen:
Data Kenmerken:
- De tabulaire features zoals
Area,Bathrooms, enBedroomsvertonen een (matig) sterke positieve lineaire correlatie met de targetPrice, wat intuïtief logisch is. Er bestaat echter ook aanzienlijke multicollineariteit tussen deze drie features. - De verdelingen van
Areaen vooralPricezijn extreem rechtsscheef. Dit wordt bevestigd door zowel de histogrammen als de scatterplot (AreavsPrice), die ook duidelijke heteroscedasticiteit laat zien (toenemende prijsvariantie bij grotere oppervlakte). - De locatiedata (
Latitude,Longitude) toont duidelijke geografische clustering, voornamelijk aan de Westkust maar ook in andere regio's. Hoewel de lineaire correlatie tussen coördinaten en prijs zwak is, suggereren de geografische plots sterke, niet-lineaire regionale prijsverschillen. Locatie is dus een cruciale factor. - Met de 1.5IQR methode zijn 23 outliers* geïdentificeerd in de
Pricevariabele, corresponderend met de extreem dure huizen in de staart van de verdeling.
- De tabulaire features zoals
Beelddata:
- De beelddata bestaat uit 4 afbeeldingen per huis. Een voorbeeld toont aan dat deze beelden informatie bevatten over de staat, stijl en locatiecontext van het huis. De kwaliteit en inhoud zullen naar verwachting variëren over de dataset.
Implicaties voor Modellering:
- Feature Belang: Alle onderzochte features (zowel tabulair als beeld) lijken potentieel relevant voor de prijsvoorspelling. De complexe, niet-lineaire relatie tussen locatie en prijs benadrukt de potentie van neurale netwerken om deze patronen te leren.
- Preprocessing:
- Schalen van de numerieke input features (Area, Bedrooms, Bathrooms, Latitude, Longitude) is essentieel voor de training van neurale netwerken.
- Er moet een beslissing worden genomen over de 23 geïdentificeerde 'Price' outliers (behouden of verwijderen), aangezien dit grote invloed kan hebben op het trainingsproces en de uiteindelijke prestaties.
- Modellen: De EDA onderstreept de relevantie van zowel tabel- als beelddata, wat de basis legt voor het ontwikkelen van de verschillende voorgeschreven modellen: een fully-connected netwerk voor tabeldata, een CNN voor beelddata, en een multimodaal model om beide te combineren. Het multimodale model heeft het potentieel om de synergie tussen de verschillende databronnen te benutten.
Deze EDA biedt een waardevol inzicht in de data en legt een basis voor de volgende stappen in het modelleringsproces, waarbij de gesignaleerde uitdagingen (met name de scheefheid van de target en de outliers) speciale aandacht vereisen.
Data Preparation¶
house_ids = df["House ID"].values
images = []
for hid in house_ids:
img_array = load_and_preprocess_image(hid, folder='Train/house')
images.append(img_array)
images = np.array(images)
Features for csv data
train = pd.read_csv('train.csv')
coastline = gpd.read_file("ne_10m_coastline.shp")
gdf_properties = gpd.GeoDataFrame(
train,
geometry=gpd.points_from_xy(train.Longitude, train.Latitude),
crs="EPSG:4326" # WGS84 Latitude/Longitude
)
gdf_properties = gdf_properties.to_crs(epsg=3857)
coastline = coastline.to_crs(epsg=3857)
train['distance_to_coast_m'] = gdf_properties.geometry.apply(
lambda point: coastline.distance(point).min()
)
print(train.head())
c:\Users\Matt\anaconda3\envs\DeepLearning\Lib\site-packages\shapely\measurement.py:72: RuntimeWarning: invalid value encountered in distance return lib.distance(a, b, **kwargs)
House ID Bedrooms Bathrooms Area Latitude Longitude Price \ 0 1 4 4.0 4053 33.6968 -111.8892 869500 1 2 4 3.0 3343 34.8280 -119.0349 865200 2 3 3 4.0 3923 33.7669 -111.9182 889000 3 4 5 5.0 4022 33.7752 -111.7791 910000 4 5 3 4.0 4116 33.7669 -111.9182 971226 distance_to_coast_m 0 350887.562039 1 77530.449791 2 356761.999625 3 366151.192977 4 356761.999625
#apply the same on test.csv
test = pd.read_csv('test.csv')
gdf_properties = gpd.GeoDataFrame(
test,
geometry=gpd.points_from_xy(test.Longitude, test.Latitude),
crs="EPSG:4326"
)
gdf_properties = gdf_properties.to_crs(epsg=3857)
test['distance_to_coast_m'] = gdf_properties.geometry.apply(
lambda point: coastline.distance(point).min()
)
c:\Users\Matt\anaconda3\envs\DeepLearning\Lib\site-packages\shapely\measurement.py:72: RuntimeWarning: invalid value encountered in distance return lib.distance(a, b, **kwargs)
plt.figure(figsize=(10, 6))
plt.hist(train['distance_to_coast_m'], bins=50, edgecolor='black')
plt.title('Histogram of Distance to Coast')
plt.xlabel('Distance to Coast (m)')
plt.ylabel('Frequency')
plt.show()
Analyse van de Verdeling 'Distance to Coast':
Feature Type: Dit is een continue numerieke variabele, berekend op basis van de geografische coördinaten (Latitude, Longitude) van elk huis. Het kwantificeert de nabijheid van een huis tot de dichtstbijzijnde kustlijn.
Verdeling: Het histogram toont een multimodale verdeling met verschillende duidelijke pieken en grote gaten ertussen:
- Piek nabij Nul: Er is een zeer grote piek bij afstanden dicht bij nul meter. Dit geeft aan dat een significant deel van de huizen in de dataset zich zeer dicht bij de kust bevindt.
- Piek rond 150 km: Een andere prominente piek ligt rond de 1.5e5 meter (150 km), wat duidt op een groep huizen die verder landinwaarts ligt, maar nog steeds relatief dichtbij een kust.
- Piek rond 400 km: Een kleinere, maar duidelijke piek is zichtbaar rond 4e5 meter (400 km).
- Verre Afstanden: Er zijn nog enkele kleinere clusters van huizen op veel grotere afstanden, met name rond 1.1e6 meter (1100 km) en 1.2e6 meter (1200 km). Er zijn grote intervallen waar vrijwel geen huizen voorkomen.
Interpretatie en Verband met Geografie: Deze multimodale verdeling weerspiegelt de geografische spreiding die we eerder zagen:
- De piek nabij nul komt waarschijnlijk overeen met de vele huizen in kustgebieden van Californië en mogelijk Washington/Oregon.
- De pieken op 150 km en 400 km representeren huizen die verder landinwaarts liggen aan de Westkust.
- De pieken op zeer grote afstanden (>1000 km) corresponderen hoogstwaarschijnlijk met de clusters die we identificeerden in het Midwesten/Zuiden (rond -90° longitude) en/of Intermountain West (rond -110° longitude), die inderdaad ver van de oceaan liggen.
Potentiële Predictieve Waarde: Afstand tot de kust is vaak een belangrijke factor in huizenprijzen, vooral in kuststaten. Men verwacht vaak dat huizen dichter bij de kust duurder zijn (ceteris paribus). Deze engineered feature 'Distance to Coast' kan dus potentieel een sterke predictor zijn voor de 'Price', mogelijk zelfs informatiever dan Latitude/Longitude alleen, omdat het een specifieke, economisch relevante geografische eigenschap kwantificeert.
Implicaties voor Modellering:
- Feature Selectie: Deze afgeleide feature 'Distance to Coast' zou zeker moeten worden overwogen als input voor de modellen die tabular data gebruiken (het fully-connected netwerk en de tabel-tak van het multimodale model).
- Preprocessing: Net als de andere numerieke features, moet ook 'Distance to Coast' geschaald worden (bv. met StandardScaler of MinMaxScaler) voordat het aan het neurale netwerk wordt gevoed.
- Relatie met Prijs: Het zou interessant zijn om de correlatie tussen 'Distance to Coast' en 'Price' te berekenen en een scatterplot te maken om hun relatie nader te onderzoeken (hoewel niet hier getoond).
Samengevat, de verdeling van 'Distance to Coast' bevestigt de geografische spreiding van de dataset (kust vs. inland) en deze feature heeft een hoge potentie als waardevolle predictor voor de huizenprijs.
distances = np.array(train['distance_to_coast_m'])
distances_reshaped = distances.reshape(-1, 1)
# fit KMeans with 2 clusters
kmeans = KMeans(n_clusters=2, random_state=0).fit(distances_reshaped)
labels = kmeans.labels_
# Create a new column in 'train' to store cluster labels
train['cluster'] = labels
# Compute mean distance for each cluster
cluster_0 = train[train['cluster'] == 0]['distance_to_coast_m']
cluster_1 = train[train['cluster'] == 1]['distance_to_coast_m']
mean_0 = cluster_0.mean()
mean_1 = cluster_1.mean()
mean_prices = train.groupby('cluster')[['Price', 'distance_to_coast_m']].mean()
print(mean_prices)
Price distance_to_coast_m cluster 0 619745.497845 8.929597e+04 1 318027.750000 1.165174e+06
Analyse van de Clustering Resultaten:
Cluster Definitie: De clustering op basis van 'distance_to_coast_m' heeft de dataset succesvol opgedeeld in twee groepen:
- Cluster 0: Huizen met een gemiddelde afstand tot de kust van circa 89,296 meter (ongeveer 89 km).
- Cluster 1: Huizen met een gemiddelde afstand tot de kust van circa 1,165,174 meter (ongeveer 1165 km).
Deze gemiddelde afstanden liggen ver uit elkaar en komen goed overeen met de pieken die we zagen in het histogram van 'Distance to Coast' (de pieken dichtbij 0-150km versus de pieken ver boven 1000km). Cluster 0 representeert dus de groep "dichtbij de kust", terwijl Cluster 1 de groep "ver landinwaarts" representeert.
Prijsverschillen tussen Clusters: Het meest opvallende resultaat is het grote verschil in gemiddelde prijs tussen de clusters:
- Cluster 0 (dichtbij kust): Gemiddelde prijs ≈ $619,745
- Cluster 1 (ver landinwaarts): Gemiddelde prijs ≈ $318,028
Huizen die dichter bij de kust liggen (Cluster 0) zijn in deze dataset gemiddeld bijna twee keer zo duur als huizen die ver landinwaarts liggen (Cluster 1).
Bevestiging van Relevantie: Dit resultaat levert sterk kwantitatief bewijs voor de hypothese dat de afstand tot de kust een significante factor is in het bepalen van de huizenprijs binnen deze dataset. Het bevestigt de potentiële predictieve waarde van de eerder geanalyseerde 'distance_to_coast_m' feature.
Implicaties voor Modellering:
- De grote prijsverschillen tussen de clusters onderstrepen nogmaals het belang van locatie-gerelateerde features (
Latitude,Longitude, en de afgeleidedistance_to_coast_m) voor het nauwkeurig voorspellen van de 'Price'. - Het resultaat versterkt de aanbeveling om de feature
distance_to_coast_mmee te nemen als input voor de neurale netwerken. - Hoewel de cluster-ID zelf als categorische feature gebruikt zou kunnen worden, bevat de continue 'distance_to_coast_m' variabele waarschijnlijk meer informatie voor een neuraal netwerk.
- De grote prijsverschillen tussen de clusters onderstrepen nogmaals het belang van locatie-gerelateerde features (
Deze clustering-analyse toont effectief aan dat er een sterke relatie bestaat tussen de nabijheid van de kust en de gemiddelde huizenprijs in deze dataset.
train.drop('cluster', axis=1, inplace=True)
#lower_bound = Q1 - 1.5*IQR
#upper_bound = Q3 + 1.5*IQR
#train = train[(train["Price"] >= lower_bound) & (train["Price"] <= upper_bound)]
#train.reset_index(drop=True, inplace=True)
#
#print(f"Shape after removing outliers: {train.shape}")
Als onderdeel van een iteratief proces hebben wij ook gemodelleerd nadat we de outliers hadden verwijderd, echter was het resultaat slechter en hebben we besloten om dit toch niet toe te passen.
Modelling¶
Fully-connected Neural Network¶
Model building¶
df = train
# Definieer features en target
feature_cols = ["Bedrooms", "Bathrooms", "Area", "Latitude", "Longitude", "distance_to_coast_m"]
target_col = "Price"
X = df[feature_cols].values
y = np.log1p(df[target_col].values)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
initial_bias = np.log1p(df[target_col].mean())
def build_model(hp):
# Kies tussen 'leaky' of 'elu' activation
activation_choice = hp.Choice('activation', values=['leaky', 'elu'])
if activation_choice == 'leaky':
def activation_layer():
return LeakyReLU(negative_slope=hp.Float('leaky_slope', 0.05, 0.2, step=0.05))
else:
def activation_layer():
return ELU(alpha=hp.Float('elu_alpha', 0.8, 1.2, step=0.1))
# Tune L2
reg = l2(hp.Float('l2_reg', 1e-5, 1e-3, sampling='LOG'))
model = Sequential([
Input(shape=(X_train.shape[1],)),
Dense(units=hp.Int('units1', min_value=64, max_value=256, step=32), kernel_regularizer=reg),
BatchNormalization(),
activation_layer(),
Dropout(rate=hp.Float('dropout1', 0.0, 0.5, step=0.05)),
Dense(units=hp.Int('units2', min_value=32, max_value=128, step=16), kernel_regularizer=reg),
BatchNormalization(),
activation_layer(),
Dropout(rate=hp.Float('dropout2', 0.0, 0.5, step=0.05)),
Dense(units=hp.Int('units3', min_value=16, max_value=64, step=16), kernel_regularizer=reg),
BatchNormalization(),
activation_layer(),
Dropout(rate=hp.Float('dropout3', 0.0, 0.5, step=0.05)),
# Outputlaag met een bias zodat de output begint dicht bij het gemiddelde van de log-transformatie
Dense(1, activation='linear', bias_initializer=tf.keras.initializers.Constant(initial_bias))
])
# Tune de learning rate
lr = hp.Float('lr', 1e-4, 1e-2, sampling='LOG')
optimizer = tf.keras.optimizers.Adam(learning_rate=lr)
model.compile(
optimizer=optimizer,
loss='mean_absolute_percentage_error',
metrics=['mean_absolute_error', 'mean_squared_error']
)
return model
tuner = kt.RandomSearch(build_model,
objective='val_loss',
max_trials=100,
executions_per_trial=5,
directory='my_tuner_dir',
project_name='house_price_tuning_extended_outliers_removed'
)
Reloading Tuner from my_tuner_dir\house_price_tuning_extended_outliers_removed\tuner0.json
tuner.search(X_train, y_train, epochs=100, validation_split=0.2,
callbacks=[EarlyStopping(patience=20, restore_best_weights=True)],
verbose=1)
best_model = tuner.get_best_models(num_models=1)[0]
best_model.summary()
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ dense (Dense) │ (None, 64) │ 448 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ batch_normalization │ (None, 64) │ 256 │ │ (BatchNormalization) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ leaky_re_lu (LeakyReLU) │ (None, 64) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout (Dropout) │ (None, 64) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 64) │ 4,160 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ batch_normalization_1 │ (None, 64) │ 256 │ │ (BatchNormalization) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ leaky_re_lu_1 (LeakyReLU) │ (None, 64) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout_1 (Dropout) │ (None, 64) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 16) │ 1,040 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ batch_normalization_2 │ (None, 16) │ 64 │ │ (BatchNormalization) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ leaky_re_lu_2 (LeakyReLU) │ (None, 16) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout_2 (Dropout) │ (None, 16) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_3 (Dense) │ (None, 1) │ 17 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 6,241 (24.38 KB)
Trainable params: 5,953 (23.25 KB)
Non-trainable params: 288 (1.12 KB)
test_loss, test_mae, test_mape = best_model.evaluate(X_test, y_test, verbose=0)
print(f"Test MAPE (log-scale): {test_mape:.4f}")
Test MAPE (log-scale): 0.2000
y_pred_log = best_model.predict(X_test)
y_pred = np.expm1(y_pred_log)
y_true = np.expm1(y_test)
mape_original = mean_absolute_percentage_error(y_true, y_pred)
print(f"Test MAPE (original scale): {mape_original*100:.2f}%")
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 44ms/step Test MAPE (original scale): 35.06%
feature_cols = ["Bedrooms", "Bathrooms", "Area", "Latitude", "Longitude", "distance_to_coast_m"]
X_test_new = test[feature_cols].values
# Predict using the best model (trained on log-transformed target values)
y_pred_log = best_model.predict(X_test_new)
# Invert the log transformation (np.expm1 is the inverse of np.log1p)
y_pred = np.expm1(y_pred_log)
submission = pd.DataFrame({
'House ID': test['House ID'],
'PredictedPrice': y_pred.flatten()
})
submission.to_csv(f'submission_nn_{dt.datetimne}.csv', index=False)
submission.head()
2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 18ms/step
| House ID | PredictedPrice | |
|---|---|---|
| 0 | 501 | 665323.75000 |
| 1 | 502 | 406676.68750 |
| 2 | 503 | 431068.15625 |
| 3 | 504 | 685641.25000 |
| 4 | 505 | 628333.06250 |
with outliers
Test MAPE (original scale): 37.64%
Kaggle score : Score: 0.59631
without outliers
Test MAPE (original scale): 28.74%
Kaggle score : Score: 0.64871
Model Visualisation¶
history = best_model.fit(X_train, y_train, epochs=100, validation_split=0.2,
callbacks=[EarlyStopping(patience=20, restore_best_weights=True)],
verbose=1)
plt.figure(figsize=(10, 6))
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training vs. Validation Loss')
plt.legend()
plt.show()
Epoch 1/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 4s 17ms/step - loss: 3.2620 - mean_absolute_error: 0.4169 - mean_squared_error: 0.3538 - val_loss: 2.7243 - val_mean_absolute_error: 0.3398 - val_mean_squared_error: 0.2044 Epoch 2/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0817 - mean_absolute_error: 0.3979 - mean_squared_error: 0.3456 - val_loss: 3.5804 - val_mean_absolute_error: 0.4430 - val_mean_squared_error: 0.3721 Epoch 3/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.5355 - mean_absolute_error: 0.4546 - mean_squared_error: 0.4003 - val_loss: 2.8392 - val_mean_absolute_error: 0.3536 - val_mean_squared_error: 0.2207 Epoch 4/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.2981 - mean_absolute_error: 0.4298 - mean_squared_error: 0.3869 - val_loss: 2.4883 - val_mean_absolute_error: 0.3128 - val_mean_squared_error: 0.1648 Epoch 5/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.3515 - mean_absolute_error: 0.4321 - mean_squared_error: 0.3676 - val_loss: 2.6178 - val_mean_absolute_error: 0.3313 - val_mean_squared_error: 0.1943 Epoch 6/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.4023 - mean_absolute_error: 0.4359 - mean_squared_error: 0.4191 - val_loss: 2.5738 - val_mean_absolute_error: 0.3236 - val_mean_squared_error: 0.1915 Epoch 7/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.5640 - mean_absolute_error: 0.4584 - mean_squared_error: 0.3913 - val_loss: 2.5435 - val_mean_absolute_error: 0.3181 - val_mean_squared_error: 0.1824 Epoch 8/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.2369 - mean_absolute_error: 0.4172 - mean_squared_error: 0.3782 - val_loss: 2.9776 - val_mean_absolute_error: 0.3692 - val_mean_squared_error: 0.2548 Epoch 9/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - loss: 3.4778 - mean_absolute_error: 0.4447 - mean_squared_error: 0.3882 - val_loss: 2.6038 - val_mean_absolute_error: 0.3260 - val_mean_squared_error: 0.1892 Epoch 10/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.2549 - mean_absolute_error: 0.4207 - mean_squared_error: 0.3166 - val_loss: 2.8512 - val_mean_absolute_error: 0.3598 - val_mean_squared_error: 0.2358 Epoch 11/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.3616 - mean_absolute_error: 0.4345 - mean_squared_error: 0.3750 - val_loss: 3.0798 - val_mean_absolute_error: 0.3875 - val_mean_squared_error: 0.2877 Epoch 12/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9982 - mean_absolute_error: 0.3872 - mean_squared_error: 0.3257 - val_loss: 3.5013 - val_mean_absolute_error: 0.4452 - val_mean_squared_error: 0.4522 Epoch 13/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0738 - mean_absolute_error: 0.3932 - mean_squared_error: 0.3144 - val_loss: 3.0238 - val_mean_absolute_error: 0.3839 - val_mean_squared_error: 0.2971 Epoch 14/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0493 - mean_absolute_error: 0.3944 - mean_squared_error: 0.3461 - val_loss: 2.7799 - val_mean_absolute_error: 0.3440 - val_mean_squared_error: 0.2248 Epoch 15/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.1152 - mean_absolute_error: 0.3970 - mean_squared_error: 0.2923 - val_loss: 2.8304 - val_mean_absolute_error: 0.3576 - val_mean_squared_error: 0.2411 Epoch 16/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0064 - mean_absolute_error: 0.3873 - mean_squared_error: 0.3021 - val_loss: 2.5757 - val_mean_absolute_error: 0.3223 - val_mean_squared_error: 0.1854 Epoch 17/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - loss: 3.6914 - mean_absolute_error: 0.4705 - mean_squared_error: 0.4079 - val_loss: 2.8874 - val_mean_absolute_error: 0.3569 - val_mean_squared_error: 0.2424 Epoch 18/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.1933 - mean_absolute_error: 0.4055 - mean_squared_error: 0.3160 - val_loss: 2.5700 - val_mean_absolute_error: 0.3206 - val_mean_squared_error: 0.1813 Epoch 19/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.0325 - mean_absolute_error: 0.3845 - mean_squared_error: 0.3074 - val_loss: 2.4601 - val_mean_absolute_error: 0.3094 - val_mean_squared_error: 0.1678 Epoch 20/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.1736 - mean_absolute_error: 0.4087 - mean_squared_error: 0.3018 - val_loss: 3.3399 - val_mean_absolute_error: 0.4258 - val_mean_squared_error: 0.4117 Epoch 21/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.1103 - mean_absolute_error: 0.4039 - mean_squared_error: 0.3500 - val_loss: 3.0422 - val_mean_absolute_error: 0.3850 - val_mean_squared_error: 0.2905 Epoch 22/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.2175 - mean_absolute_error: 0.4124 - mean_squared_error: 0.3412 - val_loss: 2.7342 - val_mean_absolute_error: 0.3442 - val_mean_squared_error: 0.2152 Epoch 23/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9518 - mean_absolute_error: 0.3805 - mean_squared_error: 0.2734 - val_loss: 2.6046 - val_mean_absolute_error: 0.3261 - val_mean_squared_error: 0.1876 Epoch 24/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.1774 - mean_absolute_error: 0.4109 - mean_squared_error: 0.3576 - val_loss: 2.8033 - val_mean_absolute_error: 0.3544 - val_mean_squared_error: 0.2312 Epoch 25/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - loss: 3.2841 - mean_absolute_error: 0.4174 - mean_squared_error: 0.3863 - val_loss: 4.5007 - val_mean_absolute_error: 0.5759 - val_mean_squared_error: 1.0512 Epoch 26/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.6315 - mean_absolute_error: 0.4631 - mean_squared_error: 0.4340 - val_loss: 3.1881 - val_mean_absolute_error: 0.4007 - val_mean_squared_error: 0.3136 Epoch 27/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.3262 - mean_absolute_error: 0.4323 - mean_squared_error: 0.4103 - val_loss: 2.6284 - val_mean_absolute_error: 0.3297 - val_mean_squared_error: 0.1988 Epoch 28/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.5892 - mean_absolute_error: 0.4590 - mean_squared_error: 0.3809 - val_loss: 3.7561 - val_mean_absolute_error: 0.4776 - val_mean_squared_error: 0.5771 Epoch 29/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.6394 - mean_absolute_error: 0.4644 - mean_squared_error: 0.3916 - val_loss: 2.8788 - val_mean_absolute_error: 0.3643 - val_mean_squared_error: 0.2481 Epoch 30/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.7854 - mean_absolute_error: 0.3587 - mean_squared_error: 0.2682 - val_loss: 2.6324 - val_mean_absolute_error: 0.3314 - val_mean_squared_error: 0.1991 Epoch 31/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.0191 - mean_absolute_error: 0.3885 - mean_squared_error: 0.3091 - val_loss: 2.9394 - val_mean_absolute_error: 0.3679 - val_mean_squared_error: 0.2527 Epoch 32/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.0946 - mean_absolute_error: 0.3997 - mean_squared_error: 0.3005 - val_loss: 2.6179 - val_mean_absolute_error: 0.3279 - val_mean_squared_error: 0.1932 Epoch 33/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 13ms/step - loss: 2.9915 - mean_absolute_error: 0.3868 - mean_squared_error: 0.2795 - val_loss: 2.4679 - val_mean_absolute_error: 0.3089 - val_mean_squared_error: 0.1649 Epoch 34/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.1091 - mean_absolute_error: 0.3999 - mean_squared_error: 0.3122 - val_loss: 2.4041 - val_mean_absolute_error: 0.3027 - val_mean_squared_error: 0.1538 Epoch 35/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0044 - mean_absolute_error: 0.3831 - mean_squared_error: 0.2863 - val_loss: 2.4651 - val_mean_absolute_error: 0.3096 - val_mean_squared_error: 0.1622 Epoch 36/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.4844 - mean_absolute_error: 0.4418 - mean_squared_error: 0.3757 - val_loss: 2.4157 - val_mean_absolute_error: 0.3054 - val_mean_squared_error: 0.1595 Epoch 37/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 13ms/step - loss: 3.0082 - mean_absolute_error: 0.3889 - mean_squared_error: 0.2857 - val_loss: 2.4880 - val_mean_absolute_error: 0.3128 - val_mean_squared_error: 0.1698 Epoch 38/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 2.9928 - mean_absolute_error: 0.3863 - mean_squared_error: 0.3096 - val_loss: 2.4030 - val_mean_absolute_error: 0.3021 - val_mean_squared_error: 0.1566 Epoch 39/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.1060 - mean_absolute_error: 0.4017 - mean_squared_error: 0.2804 - val_loss: 2.8701 - val_mean_absolute_error: 0.3661 - val_mean_squared_error: 0.2469 Epoch 40/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.8715 - mean_absolute_error: 0.3647 - mean_squared_error: 0.2898 - val_loss: 2.8884 - val_mean_absolute_error: 0.3684 - val_mean_squared_error: 0.2490 Epoch 41/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 13ms/step - loss: 2.9487 - mean_absolute_error: 0.3812 - mean_squared_error: 0.2715 - val_loss: 2.4434 - val_mean_absolute_error: 0.3086 - val_mean_squared_error: 0.1623 Epoch 42/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.1887 - mean_absolute_error: 0.4148 - mean_squared_error: 0.3214 - val_loss: 2.4574 - val_mean_absolute_error: 0.3087 - val_mean_squared_error: 0.1609 Epoch 43/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0471 - mean_absolute_error: 0.3875 - mean_squared_error: 0.2840 - val_loss: 2.4160 - val_mean_absolute_error: 0.3036 - val_mean_squared_error: 0.1553 Epoch 44/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.8576 - mean_absolute_error: 0.3684 - mean_squared_error: 0.2586 - val_loss: 2.4370 - val_mean_absolute_error: 0.3085 - val_mean_squared_error: 0.1662 Epoch 45/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.0531 - mean_absolute_error: 0.3927 - mean_squared_error: 0.3301 - val_loss: 2.8948 - val_mean_absolute_error: 0.3601 - val_mean_squared_error: 0.2448 Epoch 46/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9168 - mean_absolute_error: 0.3735 - mean_squared_error: 0.2642 - val_loss: 3.5232 - val_mean_absolute_error: 0.4361 - val_mean_squared_error: 0.3891 Epoch 47/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 2.9553 - mean_absolute_error: 0.3810 - mean_squared_error: 0.3028 - val_loss: 2.3782 - val_mean_absolute_error: 0.2991 - val_mean_squared_error: 0.1537 Epoch 48/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - loss: 3.1145 - mean_absolute_error: 0.4024 - mean_squared_error: 0.3115 - val_loss: 2.4134 - val_mean_absolute_error: 0.3019 - val_mean_squared_error: 0.1574 Epoch 49/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0944 - mean_absolute_error: 0.4011 - mean_squared_error: 0.3150 - val_loss: 2.5086 - val_mean_absolute_error: 0.3199 - val_mean_squared_error: 0.1792 Epoch 50/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.8999 - mean_absolute_error: 0.3748 - mean_squared_error: 0.2814 - val_loss: 2.9002 - val_mean_absolute_error: 0.3712 - val_mean_squared_error: 0.2625 Epoch 51/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.1292 - mean_absolute_error: 0.3991 - mean_squared_error: 0.3067 - val_loss: 3.0945 - val_mean_absolute_error: 0.3927 - val_mean_squared_error: 0.3119 Epoch 52/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.8966 - mean_absolute_error: 0.3740 - mean_squared_error: 0.2827 - val_loss: 2.9182 - val_mean_absolute_error: 0.3705 - val_mean_squared_error: 0.2755 Epoch 53/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9025 - mean_absolute_error: 0.3733 - mean_squared_error: 0.2726 - val_loss: 3.0412 - val_mean_absolute_error: 0.3885 - val_mean_squared_error: 0.3191 Epoch 54/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9330 - mean_absolute_error: 0.3816 - mean_squared_error: 0.2978 - val_loss: 2.3481 - val_mean_absolute_error: 0.2972 - val_mean_squared_error: 0.1568 Epoch 55/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9023 - mean_absolute_error: 0.3735 - mean_squared_error: 0.2689 - val_loss: 2.3041 - val_mean_absolute_error: 0.2906 - val_mean_squared_error: 0.1409 Epoch 56/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9280 - mean_absolute_error: 0.3731 - mean_squared_error: 0.2686 - val_loss: 2.4279 - val_mean_absolute_error: 0.3042 - val_mean_squared_error: 0.1586 Epoch 57/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - loss: 2.7861 - mean_absolute_error: 0.3588 - mean_squared_error: 0.2551 - val_loss: 4.0120 - val_mean_absolute_error: 0.5143 - val_mean_squared_error: 0.7554 Epoch 58/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0994 - mean_absolute_error: 0.4007 - mean_squared_error: 0.3167 - val_loss: 4.9292 - val_mean_absolute_error: 0.6323 - val_mean_squared_error: 1.3532 Epoch 59/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.4946 - mean_absolute_error: 0.4517 - mean_squared_error: 0.4069 - val_loss: 3.5575 - val_mean_absolute_error: 0.4538 - val_mean_squared_error: 0.4890 Epoch 60/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9844 - mean_absolute_error: 0.3837 - mean_squared_error: 0.2644 - val_loss: 2.4584 - val_mean_absolute_error: 0.3092 - val_mean_squared_error: 0.1588 Epoch 61/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.6125 - mean_absolute_error: 0.4619 - mean_squared_error: 0.3929 - val_loss: 2.5857 - val_mean_absolute_error: 0.3252 - val_mean_squared_error: 0.1743 Epoch 62/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.4029 - mean_absolute_error: 0.4330 - mean_squared_error: 0.3797 - val_loss: 3.2764 - val_mean_absolute_error: 0.4186 - val_mean_squared_error: 0.3607 Epoch 63/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0869 - mean_absolute_error: 0.3984 - mean_squared_error: 0.3011 - val_loss: 3.5345 - val_mean_absolute_error: 0.4544 - val_mean_squared_error: 0.4708 Epoch 64/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - loss: 3.1834 - mean_absolute_error: 0.4050 - mean_squared_error: 0.3045 - val_loss: 4.4291 - val_mean_absolute_error: 0.5678 - val_mean_squared_error: 0.9650 Epoch 65/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.2085 - mean_absolute_error: 0.4057 - mean_squared_error: 0.3110 - val_loss: 2.6208 - val_mean_absolute_error: 0.3291 - val_mean_squared_error: 0.1895 Epoch 66/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.0706 - mean_absolute_error: 0.3888 - mean_squared_error: 0.2842 - val_loss: 3.4621 - val_mean_absolute_error: 0.4293 - val_mean_squared_error: 0.3537 Epoch 67/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.3964 - mean_absolute_error: 0.4308 - mean_squared_error: 0.3794 - val_loss: 2.8203 - val_mean_absolute_error: 0.3498 - val_mean_squared_error: 0.2210 Epoch 68/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.1250 - mean_absolute_error: 0.3932 - mean_squared_error: 0.2989 - val_loss: 2.5997 - val_mean_absolute_error: 0.3242 - val_mean_squared_error: 0.1831 Epoch 69/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.0706 - mean_absolute_error: 0.3941 - mean_squared_error: 0.3081 - val_loss: 2.5540 - val_mean_absolute_error: 0.3193 - val_mean_squared_error: 0.1754 Epoch 70/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - loss: 3.0980 - mean_absolute_error: 0.3983 - mean_squared_error: 0.3430 - val_loss: 2.3651 - val_mean_absolute_error: 0.3002 - val_mean_squared_error: 0.1463 Epoch 71/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - loss: 2.8754 - mean_absolute_error: 0.3679 - mean_squared_error: 0.2494 - val_loss: 2.4261 - val_mean_absolute_error: 0.3073 - val_mean_squared_error: 0.1490 Epoch 72/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 3.6543 - mean_absolute_error: 0.4652 - mean_squared_error: 0.3907 - val_loss: 3.6300 - val_mean_absolute_error: 0.4658 - val_mean_squared_error: 0.5019 Epoch 73/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.9142 - mean_absolute_error: 0.3747 - mean_squared_error: 0.2893 - val_loss: 2.7654 - val_mean_absolute_error: 0.3533 - val_mean_squared_error: 0.2065 Epoch 74/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - loss: 3.0596 - mean_absolute_error: 0.3924 - mean_squared_error: 0.2764 - val_loss: 3.6621 - val_mean_absolute_error: 0.4719 - val_mean_squared_error: 0.4702 Epoch 75/100 10/10 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - loss: 2.7630 - mean_absolute_error: 0.3562 - mean_squared_error: 0.2372 - val_loss: 2.5084 - val_mean_absolute_error: 0.3205 - val_mean_squared_error: 0.1591
Model Explanation¶
Data Preparation¶
- Features & Target:
The input features are numerical columns such as Bedrooms, Bathrooms, Area, Latitude, Longitude, and distance to coast. The target variable is the house price, but it’s transformed using a np.log1p to stabilize variance and help the model learn better.
- Initial Bias:
The model’s final layer is initialized with a bias equal to the average log-transformed price. This gives the network a good starting point so that its initial predictions are near the mean value. (also the reason I removed outliers so that the mean is more MEANingfull)
- Additional feature:
We have added distance_to_coast_m it spans from ~1.3 km to 1,334 km – a highly skewed distribution (most houses are tens of km from the coast, with a few extremely far inland). The data of the coastline is in the github as ne_10m_coastline.shp
Model¶
- Dense Layers:
There are three fully-connected (Dense) hidden layers. The number of units in each Dense layer is tuned (for the first layer between 64 and 256 units, for the second between 32 and 128, and for the third between 16 and 64). Each Dense layer uses L2 regularization (with its strength also tuned), which helps prevent overfitting by penalizing large weight values.
- Batch Normalization:
After each Dense layer BatchNormalization is applied. This normalizes the output of the layer.
- Activation Layers:
The activation is chosen via hyperparameter tuning: If ‘leaky’ is chosen, a LeakyReLU is used (with its negative slope tuned between 0.05 and 0.2). Otherwise, an ELU is applied (with its alpha tuned between 0.8 and 1.2). These activations add non-linearity to the model, allowing it to learn complex relationships.
- Dropout Layers:
After each activation, a Dropout layer is included. Dropout randomly “turns off” a fraction of neurons during training (with the dropout rate tuned between 0 and 0.5) to help prevent overfitting.
- Output Layer:
The final Dense layer has 1 neuron with a linear activation. The bias for this layer is set to the initial bias (average of the log target) so that it starts off with predictions near the mean value.
Training¶
- Optimizer & Learning Rate:
The model uses the Adam optimizer, which adapts the learning rate for each parameter. The learning rate itself is tuned (between 1e-4 and 1e-2).
- Loss Function & Metrics:
The loss function is Mean Squared Error (MSE), which is common for regression tasks and punishes big differences to reduce MAPE.
- Early Stopping:
The training uses an EarlyStopping callback with a patience of 20 epochs. This means if the validation loss doesn’t improve for 20 epochs, training stops and the best weights are restored.
- Hyperparameter Tuning:
The model is wrapped in a Keras Tuner RandomSearch, which explores different hyperparameter combinations (like the number of units, dropout rates, learning rate, activation type, etc.) to find the best settings based on validation loss.
Conclusion¶
Based on the training vs val loss graph you can see the loss fluctuating, this could be due to not enough data or noise. You can also tell that there is likely no overfitting since the VAL loss is lower or comparable to training loss most of the time. After submitting the score on Kaggle you can see that the model is bad.
Generated with Sora AI from ChatGPT
Sources:
https://www.tensorflow.org/api_docs/python/tf/keras/losses/MeanSquaredError
https://www.tensorflow.org/api_docs/python/tf/keras/regularizers/l2
https://www.tensorflow.org/api_docs/python/tf/keras/layers/BatchNormalization
https://www.tensorflow.org/api_docs/python/tf/keras/layers/LeakyReLU
https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dropout
https://www.tensorflow.org/api_docs/python/tf/keras/optimizers/Adam
Convolutional Neural Network¶
Model building¶
df = pd.read_csv("train.csv")
# Define image size and directory
IMG_SIZE = (224, 224)
IMG_DIR = "train/houses"
# Load images and labels
def load_image(filename):
path = os.path.join(IMG_DIR, f"{str(filename)}.jpg") # Ensure filename is a string
img = load_img(path, target_size=IMG_SIZE) # Resize image
img = img_to_array(img) / 255.0 # Normalize
return img
X = np.array([load_image(f"{f}.jpg","train/house") for f in df["House ID"]])
y = np.array(df["Price"])
# Split dataset
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = models.Sequential([
layers.Conv2D(64, (3, 3), activation='relu'),
layers.MaxPooling2D((2, 2)),
BatchNormalization(),
layers.Conv2D(128, (3, 3), activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dense(512, activation='relu'),
layers.Dropout(0.2),
layers.Dense(256, activation='relu'),
layers.Dropout(0.2),
layers.Dense(1, activation='linear')
])
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
model.compile(optimizer=optimizer, loss="mean_absolute_error", metrics=["mean_absolute_percentage_error"])
stop_early = tf.keras.callbacks.EarlyStopping(monitor='val_mean_absolute_percentage_error', patience=10)
history = model.fit(X_train, y_train, epochs=100, batch_size=32, validation_data=(X_test, y_test), callbacks=[stop_early])
Epoch 1/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 14s 848ms/step - loss: 1.5193 - mean_absolute_error: 0.1139 - val_loss: 0.5611 - val_mean_absolute_error: 0.0690 Epoch 2/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 768ms/step - loss: 0.4341 - mean_absolute_error: 0.0682 - val_loss: 0.2602 - val_mean_absolute_error: 0.0587 Epoch 3/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 758ms/step - loss: 0.2184 - mean_absolute_error: 0.0571 - val_loss: 0.1466 - val_mean_absolute_error: 0.1121 Epoch 4/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 772ms/step - loss: 0.1234 - mean_absolute_error: 0.0727 - val_loss: 0.0795 - val_mean_absolute_error: 0.0744 Epoch 5/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 792ms/step - loss: 0.0699 - mean_absolute_error: 0.0635 - val_loss: 0.0452 - val_mean_absolute_error: 0.0608 Epoch 6/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 790ms/step - loss: 0.0426 - mean_absolute_error: 0.0567 - val_loss: 0.0276 - val_mean_absolute_error: 0.0576 Epoch 7/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 800ms/step - loss: 0.0284 - mean_absolute_error: 0.0609 - val_loss: 0.0178 - val_mean_absolute_error: 0.0537 Epoch 8/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 880ms/step - loss: 0.0190 - mean_absolute_error: 0.0581 - val_loss: 0.0128 - val_mean_absolute_error: 0.0577 Epoch 9/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 844ms/step - loss: 0.0144 - mean_absolute_error: 0.0581 - val_loss: 0.0119 - val_mean_absolute_error: 0.0734 Epoch 10/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 784ms/step - loss: 0.0130 - mean_absolute_error: 0.0614 - val_loss: 0.0086 - val_mean_absolute_error: 0.0608 Epoch 11/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 761ms/step - loss: 0.0097 - mean_absolute_error: 0.0571 - val_loss: 0.0073 - val_mean_absolute_error: 0.0575 Epoch 12/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 751ms/step - loss: 0.0141 - mean_absolute_error: 0.0615 - val_loss: 0.0069 - val_mean_absolute_error: 0.0602 Epoch 13/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 869ms/step - loss: 0.0079 - mean_absolute_error: 0.0573 - val_loss: 0.0063 - val_mean_absolute_error: 0.0576 Epoch 14/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 12s 889ms/step - loss: 0.0095 - mean_absolute_error: 0.0569 - val_loss: 0.0061 - val_mean_absolute_error: 0.0583 Epoch 15/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 831ms/step - loss: 0.0086 - mean_absolute_error: 0.0564 - val_loss: 0.0060 - val_mean_absolute_error: 0.0590 Epoch 16/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 806ms/step - loss: 0.0089 - mean_absolute_error: 0.0575 - val_loss: 0.0059 - val_mean_absolute_error: 0.0588 Epoch 17/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 780ms/step - loss: 0.0126 - mean_absolute_error: 0.0611 - val_loss: 0.0058 - val_mean_absolute_error: 0.0579 Epoch 18/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 827ms/step - loss: 0.0103 - mean_absolute_error: 0.0603 - val_loss: 0.0057 - val_mean_absolute_error: 0.0572 Epoch 19/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 876ms/step - loss: 0.0065 - mean_absolute_error: 0.0533 - val_loss: 0.0057 - val_mean_absolute_error: 0.0580 Epoch 20/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 805ms/step - loss: 0.0117 - mean_absolute_error: 0.0647 - val_loss: 0.0058 - val_mean_absolute_error: 0.0596 Epoch 21/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 13s 974ms/step - loss: 0.0105 - mean_absolute_error: 0.0599 - val_loss: 0.0057 - val_mean_absolute_error: 0.0580 Epoch 22/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 786ms/step - loss: 0.0123 - mean_absolute_error: 0.0634 - val_loss: 0.0056 - val_mean_absolute_error: 0.0575 Epoch 23/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 842ms/step - loss: 0.0086 - mean_absolute_error: 0.0586 - val_loss: 0.0057 - val_mean_absolute_error: 0.0584 Epoch 24/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 12s 900ms/step - loss: 0.0078 - mean_absolute_error: 0.0591 - val_loss: 0.0056 - val_mean_absolute_error: 0.0580 Epoch 25/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 12s 917ms/step - loss: 0.0074 - mean_absolute_error: 0.0573 - val_loss: 0.0056 - val_mean_absolute_error: 0.0570 Epoch 26/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 851ms/step - loss: 0.0088 - mean_absolute_error: 0.0568 - val_loss: 0.0059 - val_mean_absolute_error: 0.0608 Epoch 27/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 777ms/step - loss: 0.0077 - mean_absolute_error: 0.0558 - val_loss: 0.0056 - val_mean_absolute_error: 0.0568 Epoch 28/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 799ms/step - loss: 0.0062 - mean_absolute_error: 0.0555 - val_loss: 0.0057 - val_mean_absolute_error: 0.0587 Epoch 29/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 765ms/step - loss: 0.0129 - mean_absolute_error: 0.0637 - val_loss: 0.0058 - val_mean_absolute_error: 0.0603 Epoch 30/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 779ms/step - loss: 0.0090 - mean_absolute_error: 0.0614 - val_loss: 0.0056 - val_mean_absolute_error: 0.0568 Epoch 31/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 748ms/step - loss: 0.0064 - mean_absolute_error: 0.0544 - val_loss: 0.0057 - val_mean_absolute_error: 0.0584 Epoch 32/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 865ms/step - loss: 0.0068 - mean_absolute_error: 0.0542 - val_loss: 0.0057 - val_mean_absolute_error: 0.0590 Epoch 33/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 11s 856ms/step - loss: 0.0071 - mean_absolute_error: 0.0564 - val_loss: 0.0057 - val_mean_absolute_error: 0.0585 Epoch 34/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 757ms/step - loss: 0.0112 - mean_absolute_error: 0.0615 - val_loss: 0.0057 - val_mean_absolute_error: 0.0593 Epoch 35/100 13/13 ━━━━━━━━━━━━━━━━━━━━ 10s 779ms/step - loss: 0.0093 - mean_absolute_error: 0.0580 - val_loss: 0.0056 - val_mean_absolute_error: 0.0571
Model Visualisation¶
# Plotting training and validation loss and MAE
plt.figure(figsize=(12, 6))
# Plot training and validation loss
plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.title('Training and Validation Loss')
# Plot training and validation mean absolute error
plt.subplot(1, 2, 2)
plt.plot(history.history['mean_absolute_percentage_error'], label='Training MAPE')
plt.plot(history.history['val_mean_absolute_percentage_error'], label='Validation MAPE')
plt.xlabel('Epochs')
plt.ylabel('Mean Absolute Percentage Error')
plt.legend()
plt.title('Training and Validation MAPE')
plt.tight_layout()
plt.show()
IMG_dir = "Test/house"
test_images = []
image_names = os.listdir(IMG_dir)
# Predict
predictions = model.predict(X_test)
4/4 ━━━━━━━━━━━━━━━━━━━━ 1s 136ms/step - loss: 442702266368.0000 - mean_absolute_error: 519173.5312 Test MAE: 536191.25 4/4 ━━━━━━━━━━━━━━━━━━━━ 1s 114ms/step
# Get image ids from test set and convert to unicode
test_ids_ds = test_ds.map(lambda image, idnum: idnum).unbatch()
test_ids = next(iter(test_ids_ds.batch(NUM_TEST_IMAGES))).numpy().astype('U')
predictions = model.predict(test_images)
# Save predictions to a CSV file
df = pd.DataFrame({"House ID": image_names, "Price": predictions.flatten()})
df['House ID'] = df['House ID'].str.rstrip('.jpg')
df.to_csv("predicted_prices.csv", index=False)
Model onderbouwing¶
Een Convolutioneel Neuraal Netwerk (CNN) is een type neuraal netwerk dat speciaal is ontworpen om te werken met beelden of andere soorten grid-achtige data. Het is goed in het herkennen van patronen, zoals randen, vormen en texturen, die nodig zijn om objecten in een afbeelding te identificeren.
model = models.Sequential
Wij kiezen voor een sequential netwerk omdat deze makkelijker te maken is, en goed werkt voor CNN modellen, sequentiele modellen hebben 1 input tensor en 1 output tensor.
Bron: https://keras.io/guides/sequential_model/
layers.Conv2D(64, (3, 3), activation='relu')
Eerst maken wij een Conv2D (2D convolution layer, deze lagen worden gebruikt bij CNN modellen om 2D data te processen. Het haalt de input data en extraheert features zoals patronen, textuur en randen.
Bron: https://keras.io/api/layers/convolution_layers/convolution2d/
layers.MaxPooling2D((2, 2))
Hierna gebruiken we MaxPooling2D om de dimensies te verkleinen, hierdoor wordt het model minder gevoelig voor kleine vertalingen. Hier gebruiken wij een 2x2 grid in plaats van de 3x3 grid van hierboven.
Bron: https://keras.io/api/layers/pooling_layers/max_pooling2d/
BatchNormalization()
Batch normalisatie wordt toegepast om het model te versnellen en om de laag te normaliseren door middel van mediaan en standaard deviatie.
Bron: https://keras.io/api/layers/normalization_layers/batch_normalization/
De rest van het model bestaat uit herhalingen van Conv2D, MaxPooling2D, en BatchNormalization().
Na de convolutionele en pooling lagen wordt de data afgevlakt met de Flatten() laag.
layers.Dense(512, activation='relu')
De eerste volledig verbonden laag bevat 512 neuronen en gebruikt de ReLU-activatiefunctie om non-lineariteit in het model te brengen. Dit helpt om complexere relaties in de data te modelleren.
layers.Dropout(0.2)
Dropout wordt toegepast om overfitting te voorkomen door willekeurig 20% van de neuronen tijdens de training uit te schakelen. Waardoor het model nieuwe (ongebruikte) data pakt in plaats van de trainingsvoorbeelden.
layers.Dense(256, activation='relu')
De tweede volledig verbonden laag bevat 256 neuronen. Ook hier wordt de ReLU-activatie gebruikt.
layers.Dropout(0.2)
Hierna wordt dropout nogmaals toegepast om overfitting verder te verminderen en de robuustheid van het model te verbeteren.
layers.Dense(1, activation='linear')
De laatste laag is een outputlaag met 1 neuron en een lineaire activatiefunctie.
Omdat we hier een regressieprobleem moeten oplossen, gebruik je hier 1 neuron.
Bronnen:
Transfer Learning¶
Model building¶
# Enable eager execution
tf.config.run_functions_eagerly(True)
# Hyperparameter tuning function
def transfer_model(hp):
base_model = tf.keras.applications.ResNet50(
input_shape=(None, None, 3),
include_top=False,
weights="imagenet"
)
base_model.trainable = hp.Boolean("trainable_base", default=False)
model = models.Sequential([
layers.RandomRotation(hp.Choice("rotation", [0.0, 0.1, 0.2, 0.3])),
layers.RandomWidth(hp.Choice("width", [0.0, 0.1, 0.2, 0.3])),
layers.RandomHeight(hp.Choice("height", [0.0, 0.1, 0.2, 0.3])),
layers.RandomZoom(hp.Choice("zoom", [0.0, 0.1, 0.2, 0.3])),
layers.RandomFlip("horizontal"),
base_model,
layers.GlobalAveragePooling2D(),
layers.Dense(hp.Int("units", min_value=64, max_value=256, step=32), activation='relu',
kernel_regularizer=regularizers.l1_l2(l1=hp.Choice("l1_reg", [0.01, 0.001, 0.0001]),
l2=hp.Choice("l2_reg", [0.01, 0.001, 0.0001]))),
layers.Dropout(hp.Float("dropout", min_value=0.3, max_value=0.7, step=0.1)),
layers.Dense(1) # Output layer for regression
])
model.compile(optimizer=tf.keras.optimizers.Adam(hp.Choice("learning_rate", [0.001, 0.0001, 0.00001])),
loss='mean_absolute_percentage_error', metrics=['mae', 'mse'])
return model
# Create tuner
tuner = kt.Hyperband(transfer_model,
objective='val_loss',
max_epochs=30,
factor=3,
directory='tuner_transfer',
project_name='transfer_learning_tuning')
Reloading Tuner from tuner_transfer\transfer_learning_tuning\tuner0.json
# Start Hyperparameter Tuning met tf.data Datasets
tuner.search(train_ds,
epochs=30,
validation_data=test_ds,
callbacks=[tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5)])
# Retrieve best model
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]
best_model = tuner.hypermodel.build(best_hps)
# Train het beste model opnieuw met tf.data Datasets
history = best_model.fit(train_ds,
epochs=20,
validation_data=test_ds,
callbacks=[tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5)])
# Evaluate the model
eval_results = best_model.evaluate(X_test, y_test_scaled)
print(f"Test MAPE: {eval_results[0]}, Test MAE: {eval_results[1]}, Test MSE: {eval_results[2]}")
4/4 ━━━━━━━━━━━━━━━━━━━━ 7s 2s/step - loss: 4842.4106 - mae: 1.6148 - mse: 2.6152 Test MAPE: 4774.32666015625, Test MAE: 1.610458493232727, Test MSE: 2.6017236709594727
# Retrieve the best hyperparameters
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]
# Print the best hyperparameters
print("Best Hyperparameters:")
for param, value in best_hps.values.items():
print(f"{param}: {value}")
Best Hyperparameters: trainable_base: True rotation: 0.2 width: 0.2 height: 0.2 zoom: 0.0 units: 64 l1_reg: 0.01 l2_reg: 0.01 dropout: 0.6000000000000001 learning_rate: 0.001 tuner/epochs: 17 tuner/initial_epoch: 0 tuner/bracket: 1 tuner/round: 0
Test MAPE (original scale): 110,86%
Kaggle score : Score: 0.86549
Model Visualisation¶
from tensorflow.keras.utils import plot_model
plot_model(best_model)
You must install graphviz (see instructions at https://graphviz.gitlab.io/download/) for `plot_model` to work.
Model Explanation¶
Transfer Learning: ResNet en MobileNet¶
Bij het gebruik van transfer learning is gekozen voor ResNet als model. MobileNet werd eerst getest, maar er ontstonden problemen met de grootte van de invoerafbeeldingen. MobileNet vereist doorgaans een vaste invoergrootte, zoals 224x224 pixels, en is minder flexibel in het omgaan met variabele afbeeldingsgroottes zonder extra voorverwerking. Wij hadden vooraf gekozen voor een invoerformaat van 180x180 pixels, wat leidde tot een mismatch in de invoervereisten van MobileNet. Dit resulteerde in fouten tijdens het trainen van het model. Na het proberen van verschillende invoerformaten en het aanpassen van de afbeeldingen bleek dat niet alle afbeeldingen naar de juiste resolutie konden worden geschaald zonder verlies van beeldkwaliteit. (TensorFlow, z.d.)
ResNet is daarentegen robuuster in het verwerken van verschillende afbeeldingsformaten en behoudt sterke prestaties bij beeldherkenningstaken. Dit model maakt gebruik van residual connections, wat het trainen van diepere netwerken vergemakkelijkt en het probleem van verdwijnende gradiënten vermindert. (GeeksforGeeks, 2025)
Voordelen van Transfer Learning¶
- Snellere training: Transfer learning maakt gebruik van een vooraf getraind model, waardoor minder tijd nodig is om het model te trainen.
- Betere prestaties met beperkte data: Omdat het model al is getraind op een grote dataset, kan het goede resultaten behalen, zelfs met een kleinere trainingsset.
- Efficiëntie: Het hergebruiken van een bestaand model bespaart rekenkracht en middelen.
- Flexibiliteit: Transfer learning kan worden toegepast op verschillende domeinen door alleen de laatste lagen van het model aan te passen aan de specifieke taak.
Voordelen van ResNet¶
- Diepere netwerken mogelijk: Dankzij residual connections kunnen veel diepere netwerken worden getraind zonder problemen zoals verdwijnende gradiënten. (GeeksforGeeks, 2025)
- Betere generalisatie: ResNet presteert goed op zowel trainings- als testdata, wat wijst op een betere generalisatie. (GeeksforGeeks, 2025b)
- Flexibiliteit: Het model kan eenvoudig worden aangepast voor verschillende taken, zoals classificatie, objectdetectie en regressie.
- Efficiëntie: Ondanks de diepte blijft ResNet relatief efficiënt in termen van rekenkracht en geheugenverbruik.
- Prestaties: ResNet heeft bewezen uitstekende prestaties te leveren op benchmarks zoals ImageNet. (Kouidri, 2024)
Automatische Hyperparameter Tuning¶
Om de optimale hyperparameters te vinden, is gebruikgemaakt van Keras Tuner met Hyperband. Dit algoritme test meerdere combinaties van hyperparameters en selecteert automatisch de beste configuratie op basis van de validatieloss. In plaats van handmatig verschillende instellingen te proberen, versnelt Hyperband het proces door slimme resource allocation en early stopping.
Hoe werkt Hyperband?¶
- Snelle initiële experimenten → Veel verschillende combinaties worden kort getest.
- Successive Halving → Slecht presterende combinaties worden vroegtijdig gestopt.
- Focussen op de beste instellingen → Alleen de best presterende modellen worden verder getraind.
Beste Hyperparameters uit de tuning¶
- trainable_base:
True→ De basis van ResNet blijft trainbaar, wat fine-tuning mogelijk maakt. - rotation:
0.2→ Willekeurige rotatie van max 20% toegevoegd als data-augmentatie. - width:
0.2, height:0.2→ Willekeurige variaties in breedte en hoogte om overfitting te verminderen. - zoom:
0.0→ Geen zoom toegepast, omdat dit geen positieve impact had. - units:
64→ De dense-laag bevat 64 neuronen, wat een goede balans biedt tussen complexiteit en efficiëntie. - l1_reg:
0.01, l2_reg:0.01→ Regularisatie voorkomt overfitting door extreme gewichten te straffen. - dropout:
0.6→ 60% van de neuronen wordt uitgeschakeld tijdens training wat blijkbaar het beste resultaat oplevert, is best wel extreem veel. - learning_rate:
0.001→ De leersnelheid van de optimizer is geoptimaliseerd voor stabiele training. - tuner/epochs:
17→ Het model presteerde het beste na 17 epochs.
Met deze instellingen is het model sneller getraind, presteert het beter op nieuwe data, en wordt overfitting effectief tegengegaan.
Stappen in Transfer Learning¶
- Pretrained model laden: ResNet wordt geladen met ImageNet-gewichten.
- Feature extraction: De convolutionele basis wordt gebruikt zonder de top-lagen.
- Nieuwe lagen toevoegen: Extra dense-lagen en een global average pooling-laag.
- Hyperparameter tuning: Keras Tuner selecteert automatisch de beste instellingen.
- Model trainen: Het model wordt opnieuw getraind met de beste hyperparameters.
- Evalueren en fine-tunen: Het model wordt geëvalueerd op de testset en verder gefinetuned indien nodig.
Modeltraining en uitleg epochs¶
Het model wordt getraind met een early stopping callback, waarbij de training stopt als de validatieloss niet verbetert gedurende enkele epochs. Dit voorkomt onnodig lange trainingstijden en overfitting.
Na de training wordt het beste model opgeslagen en geëvalueerd op basis van de Mean Absolute Percentage Error (MAPE), Mean Absolute Error (MAE) en Mean Squared Error (MSE).
Bronnen¶
- TensorFlow. (z.d.). tf.keras.applications.MobileNetV2. https://www.tensorflow.org/api_docs/python/tf/keras/applications/MobileNetV2%E2%80%8B
- GeeksforGeeks. (2025, 27 januari). Residual Networks (ResNet) Deep learning. GeeksforGeeks. https://www.geeksforgeeks.org/residual-networks-resnet-deep-learning/
- GeeksforGeeks. (2025b, februari 13). Image Classification using ResNet. GeeksforGeeks. https://www.geeksforgeeks.org/image-classification-using-resnet/
- Kouidri, A. (2024, 8 maart). Mastering RESNET: Deep Learning breakthrough in image Recognition. Ikomia. https://www.ikomia.ai/blog/mastering-resnet-deep-learning-image-recognition
Model compilation¶
# Ensure the predictions array matches the length of the test DataFrame
if len(predictions) != len(test):
predictions = predictions[:len(test)]
# Write the submission file
submission = pd.DataFrame({
'House ID': test['House ID'],
'PredictedPrice': predictions.flatten()
})
submission.to_csv('submission.csv', index=False)
submission.head()
Multimodal model¶
data = train.copy()
Q1 = data['Price'].quantile(0.25)
Q3 = data['Price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
data = data[(data['Price'] >= lower_bound) & (data['Price'] <= upper_bound)]
tabular_features = data[['Bedrooms', 'Bathrooms', 'Area', 'Latitude', 'Longitude', 'distance_to_coast_m']].values
prices = np.log1p(data['Price'].values)
image_paths = ["Train/house/{}.jpg".format(id) for id in data["House ID"]]
images = []
for path in image_paths:
img = load_img(path, target_size=(224, 224)) # load image and resize to 224x224
img_array = img_to_array(img) # convert to NumPy array
images.append(img_array)
images = np.array(images, dtype="float32")
X_tab_train, X_tab_val, X_img_train, X_img_val, y_train, y_val = train_test_split(
tabular_features, images, prices, test_size=0.2, random_state=42
)
mean = X_tab_train.mean(axis=0)
std = X_tab_train.std(axis=0)
X_tab_train = (X_tab_train - mean) / std
X_tab_val = (X_tab_val - mean) / std
X_img_train /= 255.0
X_img_val /= 255.0
Model building¶
def build_model(hp):
# Image branch
img_input = Input(shape=(224, 224, 3), name='image_input')
x = tf.keras.layers.Rescaling(1.0/255)(img_input)
kernel_size = hp.Choice('kernel_size', values=[3, 5])
# Conv layer 1
filters1 = hp.Int('filters1', min_value=16, max_value=64, step=16)
x = layers.Conv2D(filters1, (kernel_size, kernel_size), activation='relu', padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling2D(pool_size=(2,2))(x)
x = layers.Dropout(hp.Float('dropout_conv1', 0.0, 0.5, step=0.1))(x)
# Conv layer 2
filters2 = hp.Int('filters2', min_value=32, max_value=128, step=32)
x = layers.Conv2D(filters2, (kernel_size, kernel_size), activation='relu', padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling2D(pool_size=(2,2))(x)
x = layers.Dropout(hp.Float('dropout_conv2', 0.0, 0.5, step=0.1))(x)
# Conv layer 3
filters3 = hp.Int('filters3', min_value=32, max_value=128, step=32)
x = layers.Conv2D(filters3, (kernel_size, kernel_size), activation='relu', padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling2D(pool_size=(2,2))(x)
x = layers.Dropout(hp.Float('dropout_conv3', 0.0, 0.5, step=0.1))(x)
# Flatten and dense (image branch)
x = layers.Flatten()(x)
img_dense_units = hp.Int('dense_img_units', min_value=64, max_value=128, step=32)
x = layers.Dense(img_dense_units, activation='relu')(x)
x = layers.Dropout(hp.Float('dropout_img_dense', 0.0, 0.5, step=0.1))(x)
img_features = x
# Tabular branch
tab_input = Input(shape=(6,), name='numeric_input')
# First Dense layer for tabular data
tab_dense1 = hp.Int('dense_tab1_units', min_value=32, max_value=128, step=32)
y = layers.Dense(tab_dense1, activation='relu')(tab_input)
y = layers.BatchNormalization()(y)
y = layers.Dropout(hp.Float('dropout_tab1', 0.0, 0.5, step=0.1))(y)
# Second Dense layer for tabular data
tab_dense2 = hp.Int('dense_tab2_units', min_value=16, max_value=64, step=16)
y = layers.Dense(tab_dense2, activation='relu')(y)
y = layers.Dropout(hp.Float('dropout_tab2', 0.0, 0.5, step=0.1))(y)
tab_features = y
# Concatenate features from both branches
combined = layers.Concatenate()([img_features, tab_features])
# Dense layers after merging
comb_dense1 = hp.Int('dense_comb1_units', min_value=32, max_value=128, step=32)
z = layers.Dense(comb_dense1, activation='relu')(combined)
z = layers.Dropout(hp.Float('dropout_comb1', 0.0, 0.5, step=0.1))(z)
comb_dense2 = hp.Int('dense_comb2_units', min_value=16, max_value=64, step=16)
z = layers.Dense(comb_dense2, activation='relu')(z)
z = layers.Dropout(hp.Float('dropout_comb2', 0.0, 0.5, step=0.1))(z)
# Output layer
output = layers.Dense(1, activation='linear')(z)
model = models.Model(inputs=[img_input, tab_input], outputs=output)
# Compile model with a tunable learning rate
lr = hp.Float('learning_rate', min_value=1e-4, max_value=1e-2, sampling='log')
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=lr),
loss='mse',
metrics=['mae', 'mape'])
return model
tuner = kt.RandomSearch(
build_model,
objective='val_mae',
max_trials=15,
executions_per_trial=2,
directory='dir_no_outliers_try2',
project_name='house_price_tuning'
)
tuner.search(
x=[X_img_train, X_tab_train], y=y_train,
validation_data=([X_img_val, X_tab_val], y_val),
epochs=80,
batch_size=32,
callbacks=[tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=20)]
)
best_model = tuner.get_best_models(num_models=1)[0]
best_hp = tuner.get_best_hyperparameters(1)[0]
print("Best hyperparameters:", best_hp.values)
Trial 16 Complete [00h 00m 08s]
Best val_mae So Far: 0.5800462365150452
Total elapsed time: 20h 24m 03s
Best hyperparameters: {'kernel_size': 3, 'filters1': 64, 'dropout_conv1': 0.30000000000000004, 'filters2': 32, 'dropout_conv2': 0.4, 'filters3': 64, 'dropout_conv3': 0.1, 'dense_img_units': 64, 'dropout_img_dense': 0.4, 'dense_tab1_units': 32, 'dropout_tab1': 0.4, 'dense_tab2_units': 32, 'dropout_tab2': 0.1, 'dense_comb1_units': 96, 'dropout_comb1': 0.1, 'dense_comb2_units': 48, 'dropout_comb2': 0.30000000000000004, 'learning_rate': 0.00035661181689187083}
best_model = tuner.get_best_models(num_models=1)[0]
best_model.summary()
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ image_input │ (None, 224, 224, │ 0 │ - │ │ (InputLayer) │ 3) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ rescaling │ (None, 224, 224, │ 0 │ image_input[0][0] │ │ (Rescaling) │ 3) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d (Conv2D) │ (None, 224, 224, │ 1,792 │ rescaling[0][0] │ │ │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ batch_normalization │ (None, 224, 224, │ 256 │ conv2d[0][0] │ │ (BatchNormalizatio… │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ max_pooling2d │ (None, 112, 112, │ 0 │ batch_normalizat… │ │ (MaxPooling2D) │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout (Dropout) │ (None, 112, 112, │ 0 │ max_pooling2d[0]… │ │ │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_1 (Conv2D) │ (None, 112, 112, │ 18,464 │ dropout[0][0] │ │ │ 32) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ batch_normalizatio… │ (None, 112, 112, │ 128 │ conv2d_1[0][0] │ │ (BatchNormalizatio… │ 32) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ max_pooling2d_1 │ (None, 56, 56, │ 0 │ batch_normalizat… │ │ (MaxPooling2D) │ 32) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_1 (Dropout) │ (None, 56, 56, │ 0 │ max_pooling2d_1[… │ │ │ 32) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_2 (Conv2D) │ (None, 56, 56, │ 18,496 │ dropout_1[0][0] │ │ │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ batch_normalizatio… │ (None, 56, 56, │ 256 │ conv2d_2[0][0] │ │ (BatchNormalizatio… │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ numeric_input │ (None, 6) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ max_pooling2d_2 │ (None, 28, 28, │ 0 │ batch_normalizat… │ │ (MaxPooling2D) │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_1 (Dense) │ (None, 32) │ 224 │ numeric_input[0]… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_2 (Dropout) │ (None, 28, 28, │ 0 │ max_pooling2d_2[… │ │ │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ batch_normalizatio… │ (None, 32) │ 128 │ dense_1[0][0] │ │ (BatchNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ flatten (Flatten) │ (None, 50176) │ 0 │ dropout_2[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_4 (Dropout) │ (None, 32) │ 0 │ batch_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense (Dense) │ (None, 64) │ 3,211,328 │ flatten[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_2 (Dense) │ (None, 32) │ 1,056 │ dropout_4[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_3 (Dropout) │ (None, 64) │ 0 │ dense[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_5 (Dropout) │ (None, 32) │ 0 │ dense_2[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ concatenate │ (None, 96) │ 0 │ dropout_3[0][0], │ │ (Concatenate) │ │ │ dropout_5[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_3 (Dense) │ (None, 96) │ 9,312 │ concatenate[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_6 (Dropout) │ (None, 96) │ 0 │ dense_3[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_4 (Dense) │ (None, 48) │ 4,656 │ dropout_6[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout_7 (Dropout) │ (None, 48) │ 0 │ dense_4[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_5 (Dense) │ (None, 1) │ 49 │ dropout_7[0][0] │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
Total params: 3,266,145 (12.46 MB)
Trainable params: 3,265,761 (12.46 MB)
Non-trainable params: 384 (1.50 KB)
test_df = pd.read_csv('test.csv')
gdf_properties = gpd.GeoDataFrame(
test_df,
geometry=gpd.points_from_xy(test_df.Longitude, test_df.Latitude),
crs="EPSG:4326"
)
gdf_properties = gdf_properties.to_crs(epsg=3857)
coastline = coastline.to_crs(epsg=3857)
test_df['distance_to_coast_m'] = gdf_properties.geometry.apply(
lambda point: coastline.distance(point).min()
)
print(train.head())
X_tab_test = test_df[['Bedrooms', 'Bathrooms', 'Area', 'Latitude', 'Longitude', 'distance_to_coast_m']].values
X_tab_test = (X_tab_test - mean) / std
image_paths_test = ["Test/{}.jpg".format(id) for id in test_df["House ID"]]
images_test = []
for path in image_paths_test:
img = load_img(path, target_size=(224, 224))
img_array = img_to_array(img)
images_test.append(img_array)
images_test = np.array(images_test, dtype="float32")
images_test /= 255.0
y_pred_log_test = best_model.predict([images_test, X_tab_test])
y_pred_test = np.expm1(y_pred_log_test)
test_df['Predicted Price'] = y_pred_test.flatten()
c:\Users\caspe\OneDrive\Documents\GitHub\DeepLearning\.venv\Lib\site-packages\shapely\measurement.py:72: RuntimeWarning: invalid value encountered in distance return lib.distance(a, b, **kwargs)
House ID Bedrooms Bathrooms Area Latitude Longitude Price \ 0 1 4 4.0 4053 33.6968 -111.8892 869500 1 2 4 3.0 3343 34.8280 -119.0349 865200 2 3 3 4.0 3923 33.7669 -111.9182 889000 3 4 5 5.0 4022 33.7752 -111.7791 910000 4 5 3 4.0 4116 33.7669 -111.9182 971226 distance_to_coast_m 0 350887.562039 1 77530.449791 2 356761.999625 3 366151.192977 4 356761.999625 2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 89ms/step
submission = pd.DataFrame({
'House ID': test_df['House ID'],
'Price': test_df['Predicted Price']
})
submission.to_csv('submission_collab_2.csv', index=False)
Dit multimodale model voorspelt huizenprijzen door een 2D beeld CNN tak te combineren met een 1D tabelgegevens tak in een netwerk. Met behulp van de functionele API accepteert het twee ingangen (een afbeelding en numerieke kenmerken) en voegt hun geleerde representaties samen voordat het een prijsvoorspelling uitvoert.
Modeluitleg¶
Voorbereiding van gegevens¶
- Kenmerken & doel:
De invoergegevens zijn numerieke kolommen zoals slaapkamers, badkamers, oppervlakte, breedtegraad, lengtegraad en afstand tot de kust. De doelvariabele is de huizenprijs, maar deze wordt getransformeerd met np.log1p om de variantie te stabiliseren en het model beter te laten leren.
- Verwijdering van uitschieters:
Extreme uitschieters werden verwijderd met de IQR-methode. Elke huizenprijs boven 1,5 × IQR van het eerste of derde kwartiel werd verwijderd. Het verwijderen van deze uitschieters voorkomt dat ze het leerproces van het model beïnvloeden en leidt tot een robuustere modelpasvorm op de resterende gegevens.
- Afbeelding voorbewerken:
Elke woningafbeelding (bijv. een foto van de woning) wordt van schijf geladen en verkleind tot 224×224 pixels. De afbeeldingen worden geconverteerd naar NumPy arrays met de vorm (224, 224, 3) en de pixelwaarden worden genormaliseerd naar het bereik [0,1].
- Extra functie:
We hebben distance_to_coast_m toegevoegd deze strekt zich uit van ~1,3 km tot 1.334 km - een zeer scheve verdeling (de meeste huizen liggen tientallen km van de kust, met een paar extreem ver landinwaarts). De gegevens van de kustlijn staan op github als ne_10m_coastline.shp
Vertaald met DeepL.com (gratis versie)
Het model is opgebouwd uit twee invoertakken die later worden samengevoegd, waardoor het tegelijkertijd kan leren van gegevens uit afbeeldingen en tabellen bron
Beeldtak: De beeldinvoer (224×224×3) wordt verwerkt door drie convolutielagen. Elke convolutielaag (met instelbare filters en kernelgroottes) gebruikt ReLU-activering, gevolgd door Batch Normalisatie, MaxPooling en Dropout. De resulterende kenmerkkaarten worden afgevlakt en ingevoerd in een dichte laag (met drop-out) om visuele kenmerken te extraheren.
Vertakking in tabelvorm: De zes gestandaardiseerde kenmerken gaan door twee dichte lagen (met batch-normalisatie en drop-out) om een representatie van de gestructureerde gegevens te leren.
Uitvoer: Uitgangen van beide takken worden samengevoegd en verwerkt door twee extra dichte lagen met drop-out voor een laatste uitvoerlaag. Deze uitvoerlaag heeft een enkel neuron met lineaire activering dat de log-getransformeerde huizenprijs voorspelt.
Training¶
- Optimizer en leersnelheid:
Het model gebruikt de Adam optimizer, die de leersnelheid voor elke parameter aanpast. De leersnelheid zelf wordt getuned.
- Verliesfunctie & Metriek:
De verliesfunctie is Mean Squared Error (MSE), wat gebruikelijk is voor regressietaken en grote verschillen bestraft om MAPE te verminderen.
De training gebruikt een EarlyStopping callback met een geduld van 20 epochs. Dit betekent dat als het validatieverlies niet verbetert gedurende 20 epochs, de training stopt en de beste gewichten worden hersteld.
- Hyperparameter tuning:
Het model is verpakt in een Keras Tuner RandomSearch, die verschillende hyperparametercombinaties onderzoekt (zoals het aantal eenheden, uitvalpercentages, leersnelheid, activeringstype, etc.) om de beste instellingen te vinden op basis van validatieverlies.
Generated with Sora AI from ChatGPT
Bevindingen¶
Wij zijn er achter gekomen dat het laaste model, de Multimodal neural network het beste werkte. Hiervoor hebben wij KerasTuner(RandomSearch) gebruikt, hieruit zijn de volgende parameters als beste gevonden:
| Parameter | Value |
|---|---|
| Kernel Size | 3 |
| Filters 1 | 64 |
| Dropout Conv 1 | 0.30 |
| Filters 2 | 32 |
| Dropout Conv 2 | 0.40 |
| Filters 3 | 64 |
| Dropout Conv 3 | 0.10 |
| Dense Img Units | 64 |
| Dropout Img Dense | 0.40 |
| Dense Tab 1 Units | 32 |
| Dropout Tab 1 | 0.40 |
| Dense Tab 2 Units | 32 |
| Dropout Tab 2 | 0.10 |
| Dense Comb 1 Units | 96 |
| Dropout Comb 1 | 0.10 |
| Dense Comb 2 Units | 48 |
| Dropout Comb 2 | 0.30 |
| Learning Rate | 0.00035661181689187083 |
Wij hebben gemerkt dat outliers verwijderen het multi-modale NN helpt met betere resultaten maken, daarintegen werkte data augmentatie dit model juist tegen, waardoor we het niet voor het eindmodel gehouden hebben.
Conclusie¶
Modelleringsproces en Uitdagingen¶
Voor deze opdracht hebben we een neuraal netwerkmodel ontwikkeld om woningprijzen te voorspellen. Tijdens dit proces kwamen we een aantal uitdagingen tegen:
- Traintijd: Het trainen van het model duurde lang, vooral bij complexere architecturen. Dit maakte het testen van verschillende instellingen tijdrovend.
- Feature-engineering: Het vinden van de juiste kenmerken was cruciaal. Sommige features hadden een grote impact op de prijs, terwijl andere weinig bijdroegen.
- Hoeveelheid data: Hoewel de dataset groot was, bleef het een uitdaging om voldoende representatieve data te hebben voor alle woningtypes.
- Balans tussen tabulaire en visuele data: Het combineren van numerieke woninggegevens met afbeeldingen bleek complex. Sommige modellen gaven te veel gewicht aan één van de twee, wat de voorspellingen minder accuraat maakte.
Effectiviteit van Verschillende Modelarchitecturen¶
We hebben meerdere modelarchitecturen en technieken getest om de prestaties te verbeteren.
Effectieve technieken:
- Multi-modale neurale netwerken: Dit type model, dat zowel tabulaire als visuele data combineert, presteerde het beste. Door beide soorten informatie te integreren, kon het model betere voorspellingen maken.
- Afstand tot de kust als feature: Woningen dichter bij de kust waren aanzienlijk duurder, dus deze variabele bleek een belangrijke toevoeging.
- Dropout en batchnormalisatie: Hielpen om overfitting te voorkomen en verbeterden de stabiliteit van het model.
Minder effectieve technieken:
- CNN’s alleen: Hoewel convolutionele netwerken goed werken met beelddata, bleken ze minder effectief zonder aanvullende tabulaire data.
- Data augmentation: Het genereren van extra beelddata door rotatie en schaling had weinig effect, waarschijnlijk omdat woningafbeeldingen minder variatie vereisen dan bijvoorbeeld objectherkenningstaken.
- Te diepe netwerken: Dit zorgde voor lange traintijden en soms zelfs slechtere prestaties door overfitting.
Door deze experimenten werd duidelijk dat een multi-modaal model, met een juiste selectie van kenmerken en een gebalanceerde architectuur, het beste werkte.
Advies voor Makelaars¶
Op basis van deze resultaten kunnen makelaars gerichter te werk gaan bij prijsbepalingen en verkoopstrategieën:
- Neem de afstand tot de kust mee in waardebepalingen: Dit bleek een sterke invloed te hebben op de woningprijzen.
- Combinatie van data geeft de beste inzichten: Zowel tabulaire kenmerken (zoals oppervlakte en locatie) als visuele kenmerken (uit foto’s) zijn belangrijk bij prijsbepalingen.
- Gebruik data-analyse voor prijsstrategie: Door modellen te gebruiken, kunnen makelaars beter voorspellen welke prijs realistisch is voor een woning.
Advies voor Huizenkopers en Huizenverkopers¶
Voor kopers en verkopers bieden deze inzichten waardevolle handvatten:
- Kopers:
- Woningen dichter bij de kust zijn vaak duurder, houd hier rekening mee bij je zoekcriteria.
- Kijk niet alleen naar de vraagprijs, maar ook naar vergelijkbare woningen en hun kenmerken.
- Verkopers:
- Zorg voor goede foto’s en een nette presentatie om de woning aantrekkelijker te maken.
- Baseer de vraagprijs op objectieve data in plaats van alleen op gevoel, en kijk naar trends in de markt.
Door deze inzichten te gebruiken, kunnen makelaars, kopers en verkopers beter onderbouwde beslissingen nemen op basis van data.